package junior.util.excel.exporter;

import com.google.common.collect.Maps;
import junior.util.function.CheckedBiConsumer;
import junior.util.io.Closer;
import junior.util.reflect.ReflectUtils;
import junior.util.value.ConvertParam;
import junior.util.value.ValueUtils;

import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author wubo
 * @date 2023/5/11 17:16
 */
public class CsvFileWriter implements AutoCloseable {
    private final static String CRLF = "\r\n";
    private final static String NEED_QUOTE = "^.*?[\r\n,]+.*?$";
    private int dataRows;
    private boolean initialized;
    private int rowIndex;
    private boolean dataTypeIsMap;
    private Class<?> dataType;
    private Map<String, Integer> colNameMap;
    private String[] fieldNameArray;
    private ConvertParam convertParam = new ConvertParam();
    
    private CheckedBiConsumer<Object, Integer> rowNoSetting;
    private CheckedBiConsumer<Object, Integer> sheetNoSetting;
    private ExcelExportConfig cfg;
    private BufferedWriter bufferedWriter;
    private OutputStreamWriter osw;
    
    public CsvFileWriter(OutputStream os, ExcelExportConfig cfg) throws Exception {
        this.osw = new OutputStreamWriter(os, cfg.charset);
        this.bufferedWriter = new BufferedWriter(this.osw);
        this.cfg = cfg;
    }
    
    public <T> void writeData(List<T> list) throws Exception {
        if (list != null && list.size() > 0) {
            if (!initialized) {
                init(list.get(0));
            }
            int maxDataRows = cfg.getMaxDataRows() > 0 ? cfg.getMaxDataRows() : Integer.MAX_VALUE;
            if (dataRows > maxDataRows) {
                return;
            }
            
            int sheetIndex = 0;
            int rowNoIndex = 1;
            for (T t : list) {
                int currentRowIndex = rowNoIndex ++;
                if (rowNoSetting != null) {
                    rowNoSetting.accept(t, currentRowIndex);
                }
                if (sheetNoSetting != null) {
                    sheetNoSetting.accept(t, sheetIndex);
                }
                writeRow(t, fieldNameArray, cfg);
                dataRows++;
                if (dataRows > maxDataRows) {
                    return;
                }
            }
            bufferedWriter.flush();
        }
    }
    
    private <T> void init(T t) throws Exception {
        if (t != null) {
            if (cfg.autoMapColName) {
                Class<?> clazz = t.getClass();
                boolean dataTypeIsMap = Map.class.isAssignableFrom(clazz);
                autoMapColNames(cfg, t, dataTypeIsMap);
            }
        }
        // field --> columnIdx
        Map<String, Integer> colNameMap = getSortedColNames(cfg);
        
        if (t != null) {
            Class<?> clazz = t.getClass();
            boolean dataTypeIsMap = Map.class.isAssignableFrom(clazz);
            
            this.fieldNameArray = colNameMap.keySet().toArray(new String[colNameMap.size()]);
            this.colNameMap = colNameMap;
            this.dataTypeIsMap = dataTypeIsMap;
            this.dataType = clazz;
            
            if (cfg.rowNoName != null) {
                if (dataTypeIsMap) {
                    rowNoSetting = (data, row) -> ((Map) data).put(cfg.rowNoName, row);
                } else {
                    Method rowNoSetter = ReflectUtils.setter4Safe(dataType, cfg.rowNoName);
                    Class<?> valueDataType = rowNoSetter.getParameterTypes()[0];
                    if (rowNoSetter != null) {
                        rowNoSetting = (data, row) -> rowNoSetter.invoke(data, ValueUtils.convert(row, valueDataType));
                    }
                }
            }
            if (cfg.sheetNoName != null) {
                if (dataTypeIsMap) {
                    sheetNoSetting = (data, row) -> ((Map) data).put(cfg.sheetNoName, row);
                } else {
                    Method sheetNoSetter = ReflectUtils.setter4Safe(dataType, cfg.sheetNoName);
                    Class<?> valueDataType = sheetNoSetter.getParameterTypes()[0];
                    if (sheetNoSetter != null) {
                        sheetNoSetting = (data, row) -> sheetNoSetter.invoke(data, ValueUtils.convert(row, valueDataType));
                    }
                }
            }
        }
        
        if (cfg.writeHeader && cfg.getHeaderRow() != -99) {
            convertParam.dateFormatPattern(cfg.getDateFormat()).arraySeparator(cfg.delimiter);
            writeRow(cfg.getColIndexMap(), cfg);
        }
        if (cfg.writeTitle || (cfg.writeHeader && cfg.getHeaderRow() == -99)) { // 括号内条件为兼容旧版本
            convertParam.dateFormatPattern(cfg.getDateFormat()).arraySeparator(cfg.delimiter);
            writeRow(colNameMap, cfg);
        }
        
        this.rowIndex = cfg.getDataStartRow();
        this.initialized = true;
    }
    
    private <T> void writeRow(T t, String[] fieldNameArray, ExcelExportConfig cfg) throws Exception {
        StringBuilder line = new StringBuilder();
        int index = 0;
        for (String fieldName : fieldNameArray) {
            if (fieldName == null) {
                continue;
            }
            Object value = null;
            if (dataTypeIsMap) {
                Map map = (Map) t;
                value = map.get(fieldName);
            } else {
                value = getCellValue(t, fieldName);
            }
            if (index++ > 0) {
                line.append(",");
            }
            if (value == null) {
                line.append("");
            } else if (value instanceof String) {
                line.append(transferIfNecessary((String) value));
            } else {
                convertParam.dataFormat(cfg.getDataFormat(fieldName));
                convertParam.dateFormatPattern(ValueUtils.ifNull(cfg.getDateFormat(fieldName), cfg.dateFormat));
                line.append(ValueUtils.convert(value, String.class, convertParam));
            }
        }
        line.append(CRLF);
        bufferedWriter.write(line.toString());
    }
    
    private <T> void writeRow(Map<String, Integer> sortedColNames, ExcelExportConfig cfg) throws Exception {
        StringBuilder line = new StringBuilder();
        int index = 0;
        for (String colName : sortedColNames.keySet()) {
            if (colName != null) {
                if (cfg.colNameMap.containsKey(colName)) {
                    colName = cfg.colNameMap.get(colName);
                }
            }
            if (index++ > 0) {
                line.append(",");
            }
            line.append(transferIfNecessary(colName));
        }
        line.append(CRLF);
        bufferedWriter.write(line.toString());
    }
    
    private String transferIfNecessary(String s) {
        if (s.matches(NEED_QUOTE)) {
            return "\"" + s.replace("\"", "\"\"") + "\"";
        }
        return s;
    }
    
    private Object getCellValue(Object obj, String name) {
        if (obj == null) return null;
        
        if (name.contains(".")) {
            int idx = name.indexOf(".");
            String subObjAttrName = name.substring(0, idx);
            return getCellValue(ReflectUtils.callGetMethod(obj, subObjAttrName), name.substring(idx + 1));
        } else {
            return ReflectUtils.callGetMethod(obj, name);
        }
    }
    
    private Map<String, Integer> getSortedColNames(ExcelExportConfig cfg) {
        Map<String, Integer> colNames = Maps.newHashMap();
        int colIdx = 0;
        if (!cfg.colIndexMap.isEmpty()) {
            colNames.putAll(cfg.colIndexMap);
            colIdx = Collections.max(cfg.colIndexMap.values());
        }
        for (String key : cfg.colNameMap.keySet()) {
            if (!colNames.containsKey(key)) {
                colNames.put(key, colIdx++);
            }
        }
        // sort
        sortColNames(colNames, cfg);
        return colNames;
    }
    
    private void sortColNames(Map<String, Integer> colNames, ExcelExportConfig cfg) {
        Map<String, Integer> copied = new HashMap<String, Integer>(colNames);
        Map<Integer, String> colIdxMap = new HashMap<Integer, String>();
        for (Map.Entry<String, Integer> entry : colNames.entrySet()) {
            colIdxMap.put(entry.getValue(), entry.getKey());
        }
        // sort
        for (String colName : copied.keySet()) {
            if (cfg.colIndexMap.containsKey(colName)) {
                int colIdx = cfg.colIndexMap.get(colName);
                int curIdx = colNames.get(colName);
                if (curIdx != colIdx) {
                    String replaced = colIdxMap.get(colIdx);
                    if (replaced != null) colNames.put(replaced, curIdx);
                    colNames.put(colName, colIdx);
                }
            }
        }
    }
    
    private void autoMapColNames(ExcelExportConfig cfg, Object t, boolean dataTypeIsMap) {
        Set<String> ignorePropertySet = cfg.ignoreProperties != null ? new HashSet<String>(Arrays.asList(cfg.ignoreProperties)) : Collections.EMPTY_SET;
        if (dataTypeIsMap) {
            Map<String, Object> map = (Map) t;
            
            for (String key : map.keySet()) {
                if (ignorePropertySet.contains(key)) {
                    continue;
                }
                if (!cfg.getColNameMap().containsKey(key) && !cfg.getColIndexMap().containsKey(key)) {
                    cfg.mapColName(key, key);
                }
            }
        } else {
            String[] getterNames = ReflectUtils.getterNames(t.getClass());
            
            for (String getterName : getterNames) {
                if (ignorePropertySet.contains(getterName)) {
                    continue;
                }
                if (!cfg.getColNameMap().containsKey(getterName) && !cfg.getColIndexMap().containsKey(getterName)) {
                    cfg.mapColName(getterName, getterName);
                }
            }
        }
    }
    
    @Override
    public void close() throws Exception {
        Closer.close(osw, bufferedWriter);
    }
}
